Skip to content

Comments

feat(#333): 브랜드 검색 api 추가#349

Merged
yerimi00 merged 5 commits intodevfrom
feat/333-brand-search
Feb 10, 2026
Merged

feat(#333): 브랜드 검색 api 추가#349
yerimi00 merged 5 commits intodevfrom
feat/333-brand-search

Conversation

@yerimi00
Copy link
Contributor

@yerimi00 yerimi00 commented Feb 9, 2026

Summary

/api/v1/brands/search 엔드포인트를 추가해 매칭된 브랜드를 브랜드명(title)로 검색할 수 있게 했고, sortBy, category, tags 필터를 동일하게 적용했습니다. 검색 결과는 페이지네이션(page, size)을 지원합니다.

Changes

  • /api/v1/brands/search API 신규 추가
  • 브랜드명(title) 검색 로직 추가 (브랜드명만 대상)
  • sortBy, category, tags 필터 동일 적용
  • TOP 10 제한 제거 및 페이지네이션 적용
  • Swagger 문서 업데이트

Type of Change

  • Bug fix (기존 기능에 영향을 주지 않는 버그 수정)
  • New feature (기존 기능에 영향을 주지 않는 새로운 기능 추가)
  • Breaking change (기존 기능에 영향을 주는 수정)
  • Refactoring (기능 변경 없는 코드 개선)
  • Documentation (문서 수정)
  • Chore (빌드, 설정 등 기타 변경)
  • Release (develop → main 배포)

Related Issues

Closes #333

참고 사항

@yerimi00 yerimi00 linked an issue Feb 9, 2026 that may be closed by this pull request
@gemini-code-assist
Copy link
Contributor

Warning

Gemini encountered an error creating the summary. You can try again by commenting /gemini summary.

@yerimi00 yerimi00 self-assigned this Feb 9, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

이 Pull Request는 사용자가 브랜드명으로 브랜드를 검색할 수 있는 새로운 API 엔드포인트를 도입하여 브랜드 탐색 기능을 확장합니다. 기존의 정렬 및 필터링 옵션을 통합하고 페이지네이션을 지원함으로써, 사용자는 더욱 유연하고 효율적으로 원하는 브랜드를 찾을 수 있게 됩니다. 이는 전반적인 사용자 경험을 향상시키고 애플리케이션의 검색 기능을 강화하는 데 기여합니다.

Highlights

  • 새로운 브랜드 검색 API 추가: /api/v1/brands/search 엔드포인트가 추가되어 브랜드명(title)으로 브랜드를 검색할 수 있게 되었습니다.
  • 검색 필터 및 정렬 기능: 기존의 sortBy, category, tags 필터가 검색 API에도 동일하게 적용됩니다.
  • 페이지네이션 지원: 검색 결과에 페이지네이션(page, size)이 적용되어 대량의 검색 결과도 효율적으로 처리할 수 있습니다.
  • TOP 10 제한 제거: 기존 브랜드 조회 로직에 있던 TOP 10 제한이 검색 API에서는 제거되었습니다.
  • Swagger 문서 업데이트: 새로 추가된 브랜드 검색 API에 대한 Swagger 문서가 업데이트되었습니다.
Changelog
  • src/main/java/com/example/RealMatch/brand/presentation/controller/BrandController.java
    • 새로운 브랜드 검색 GET 엔드포인트 '/search'를 추가했습니다.
    • MatchService를 주입하여 브랜드 검색 로직을 호출하도록 했습니다.
    • 요청 파라미터로 title, sortBy, category, tags, page, size를 받도록 설정했습니다.
  • src/main/java/com/example/RealMatch/brand/presentation/swagger/BrandSwagger.java
    • 새로운 브랜드 검색 API에 대한 Swagger 문서 정의를 추가했습니다.
    • 검색 API의 요약, 설명, 응답 코드 및 모든 요청 파라미터에 대한 설명을 포함했습니다.
    • 기존 '브랜드 개별 요약 조회 API'의 summary에 'by 이예림'을 추가했습니다.
  • src/main/java/com/example/RealMatch/match/application/service/MatchService.java
    • 브랜드 검색을 위한 'searchMatchingBrands' 메서드 시그니처를 추가했습니다.
  • src/main/java/com/example/RealMatch/match/application/service/MatchServiceImpl.java
    • StringUtils 유틸리티 클래스를 임포트했습니다.
    • 기존 'getMatchingBrands' 메서드에 태그 필터링 로직을 추가했습니다.
    • 새로운 'searchMatchingBrands' 메서드를 구현하여 브랜드명 검색, 카테고리/태그 필터링, 정렬 및 페이지네이션 로직을 포함했습니다.
    • 브랜드명으로 필터링하는 'filterBrandByTitle' 헬퍼 메서드를 추가했습니다.
    • 태그로 필터링하는 'filterBrandByTags' 헬퍼 메서드를 추가했습니다.
Activity
  • gemini-code-assist[bot]이 요약 생성 중 오류가 발생했습니다.
  • Yoonchulchung님이 요약 생성을 다시 요청했습니다.

@Yoonchulchung
Copy link
Member

/gemini review

@Yoonchulchung
Copy link
Member

@Gemini-bot

@Yoonchulchung
Copy link
Member

/re-review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이번 PR은 브랜드 검색 API를 추가하고 페이지네이션을 적용하는 중요한 기능 개선을 포함하고 있습니다. 전반적으로 기능 구현은 잘 되었지만, 몇 가지 심각한 성능 문제를 발견했습니다. 특히, searchMatchingBrands 메소드에서 N+1 쿼리 문제와 모든 데이터를 메모리에 로드하여 처리하는 방식은 서비스 확장성에 큰 걸림돌이 될 수 있습니다. 이 부분은 데이터베이스 레벨에서 처리하도록 리팩토링이 시급하며, 이는 기존 코드 리뷰 규칙과도 일치합니다. 또한, 태그 필터링 로직의 효율성을 개선할 수 있는 부분도 제안했습니다.

자세한 내용은 각 코드 리뷰 코멘트를 참고해주세요.

Comment on lines 529 to 538
List<Long> brandIds = brandHistories.stream()
.map(h -> h.getBrand().getId())
.toList();
Map<Long, List<String>> brandDescribeTagMap = brandIds.stream()
.collect(Collectors.toMap(
brandId -> brandId,
brandId -> brandDescribeTagRepository.findAllByBrandId(brandId).stream()
.map(BrandDescribeTag::getBrandDescribeTag)
.toList()
));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

brandDescribeTagMap을 생성하는 과정에서 N+1 쿼리 문제가 발생합니다. brandIds 리스트를 순회하며 각 brandId에 대해 brandDescribeTagRepository.findAllByBrandId(brandId)를 호출하고 있어 브랜드 수만큼 쿼리가 실행됩니다. 이로 인해 심각한 성능 저하가 발생할 수 있습니다.

모든 태그를 한 번의 쿼리로 가져온 후 메모리에서 그룹화하는 방식으로 수정해야 합니다.

BrandDescribeTagRepositoryfindAllByBrandIdIn(List<Long> brandIds) 메소드를 추가하고, 이를 사용하여 아래와 같이 수정하는 것을 권장합니다.

// 1. BrandDescribeTagRepository.java 에 다음 메소드 추가
List<BrandDescribeTag> findAllByBrandIdIn(List<Long> brandIds);

// 2. MatchServiceImpl.java 수정
List<Long> brandIds = brandHistories.stream()
        .map(h -> h.getBrand().getId())
        .toList();

List<BrandDescribeTag> allTags = brandDescribeTagRepository.findAllByBrandIdIn(brandIds);

Map<Long, List<String>> brandDescribeTagMap = allTags.stream()
        .collect(Collectors.groupingBy(
                tag -> tag.getBrand().getId(),
                Collectors.mapping(BrandDescribeTag::getBrandDescribeTag, Collectors.toList())
        ));
References
  1. To prevent N+1 query issues, fetch multiple items in a single batch query (e.g., using an 'IN' clause) instead of querying for each item within a loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 리뷰대로 반영했습니다.

Comment on lines 540 to 554
List<MatchBrandResponseDto.BrandDto> filteredBrands = brandHistories.stream()
.filter(history -> filterBrandByCategory(history.getBrand(), category))
.filter(history -> filterBrandByTitle(history.getBrand(), title))
.filter(history -> filterBrandByTags(history.getBrand().getId(), tags, brandDescribeTagMap))
.sorted(getBrandHistoryComparator(sortBy, brandLikeCountMap))
.map(history -> toMatchBrandDtoFromHistory(history, likedBrandIds, recruitingBrandIds, brandDescribeTagMap))
.toList();

int total = filteredBrands.size();
int safePage = Math.max(page, 0);
int safeSize = size <= 0 ? 20 : size;
int fromIndex = safePage * safeSize;
int toIndex = Math.min(fromIndex + safeSize, total);
List<MatchBrandResponseDto.BrandDto> pagedBrands =
fromIndex >= total ? List.of() : filteredBrands.subList(fromIndex, toIndex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

필터링, 정렬, 페이지네이션을 모두 메모리에서 처리하고 있습니다. brandHistories에서 모든 브랜드 정보를 가져온 후 애플리케이션에서 처리하는 방식은 데이터 양이 증가하면 심각한 성능 저하와 메모리 부족(OOM)을 유발할 수 있습니다.

이러한 작업은 데이터베이스 레벨에서 처리하는 것이 효율적입니다. Specification이나 QueryDSL을 사용하거나, 복잡한 JPQL 쿼리를 작성하여 데이터베이스가 필터링, 정렬, 페이지네이션을 모두 처리하도록 수정하는 것을 강력히 권장합니다. 이렇게 하면 애플리케이션의 부하를 크게 줄이고 확장성을 확보할 수 있습니다.

References
  1. Avoid filtering large collections in memory; instead, push filtering logic down to the database layer by adding specific query methods to the repository for better performance.

Comment on lines +686 to +704
private boolean filterBrandByTags(Long brandId, List<String> tags, Map<Long, List<String>> brandDescribeTagMap) {
if (tags == null || tags.isEmpty()) {
return true;
}
List<String> brandTags = brandDescribeTagMap.getOrDefault(brandId, List.of());
if (brandTags.isEmpty()) {
return false;
}

List<String> normalizedBrandTags = brandTags.stream()
.filter(StringUtils::hasText)
.map(tag -> tag.trim().toLowerCase())
.toList();

return tags.stream()
.filter(StringUtils::hasText)
.map(tag -> tag.trim().toLowerCase())
.anyMatch(normalizedBrandTags::contains);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

filterBrandByTags 메소드에서 태그를 비교하는 로직이 비효율적입니다. normalizedBrandTagsList인 상태에서 contains를 호출하면 내부적으로 반복문이 실행되어 O(N*M)의 시간 복잡도를 가집니다 (N: 필터 태그 수, M: 브랜드 태그 수).

normalizedBrandTagsSet으로 변환하여 contains 호출 시 O(1)의 시간 복잡도를 갖도록 하거나, Collections.disjoint를 사용하여 두 컬렉션에 공통 요소가 있는지 확인하는 것이 더 효율적이고 코드가 간결해집니다.

private boolean filterBrandByTags(Long brandId, List<String> tags, Map<Long, List<String>> brandDescribeTagMap) {
    if (tags == null || tags.isEmpty()) {
        return true;
    }
    List<String> brandTags = brandDescribeTagMap.getOrDefault(brandId, List.of());
    if (brandTags.isEmpty()) {
        return false;
    }

    List<String> normalizedFilterTags = tags.stream()
            .filter(StringUtils::hasText)
            .map(tag -> tag.trim().toLowerCase())
            .toList();

    List<String> normalizedBrandTags = brandTags.stream()
            .filter(StringUtils::hasText)
            .map(tag -> tag.trim().toLowerCase())
            .toList();

    return !java.util.Collections.disjoint(normalizedBrandTags, normalizedFilterTags);
}

@yerimi00
Copy link
Contributor Author

/re-review

@1000hyehyang
Copy link
Member

/gemini review @gemini-code-assist 빨리 전반적인 리뷰를 다시 하란말이다!!! 코드 품질 개선할 점이 있는지, 시스템 장애 문제 잠재 가능성이 없는지 확인해.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

새로운 브랜드 검색 API가 성공적으로 추가되었습니다. QueryDSL을 활용하여 검색, 필터링, 정렬 및 페이지네이션 기능을 효율적으로 구현했습니다. 특히 N+1 쿼리 문제를 방지하기 위한 findAllByBrandIdIn 메서드 사용과 QueryDSL의 JPAExpressions.exists()를 이용한 태그 필터링 로직은 매우 인상적입니다. 전반적으로 깔끔하고 효율적인 코드 변경입니다.

@yerimi00 yerimi00 merged commit d93a3bb into dev Feb 10, 2026
1 check passed
@yerimi00 yerimi00 deleted the feat/333-brand-search branch February 10, 2026 05:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 브랜드 검색 api 개발

3 participants